package com.gjy.mongod.codec;

import com.gjy.mongod.domain.BaseEntity;
import org.bson.*;
import org.bson.codecs.*;
import org.bson.codecs.configuration.CodecProvider;
import org.bson.codecs.configuration.CodecRegistries;
import org.bson.codecs.configuration.CodecRegistry;
import org.bson.types.Decimal128;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Field;
import java.lang.reflect.ParameterizedType;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;

/**
 * @author 宫静雨
 * @version 1.0
 * @since 2023-10-17 10:22:40
 */
public class SpringProvider implements CodecProvider {

    private static final Logger log = LoggerFactory.getLogger(SpringProvider.class);

    @Override
    public <T> Codec<T> get(Class<T> cls, CodecRegistry codecRegistry) {
        log.info("被解析的类型为: {}", cls);
        if (Objects.nonNull(cls)) {
            return new SpringCodec<>(cls);
        }
        return null;
    }

    private static final class SpringCodec<T> implements Codec<T> {

        private static final String ID_FIELD_NAME = "_id";
        private static final CodecRegistry DEFAULT_REGISTRY = CodecRegistries.fromProviders(
                new ValueCodecProvider(), new BsonValueCodecProvider(), new DocumentCodecProvider(),
                new CustomerProvider()
        );
        private final CodecRegistry registry;
        private final BsonTypeCodecMap bsonTypeCodecMap;
        private final Transformer valueTransformer;

        private final Class<T> cls;

        public SpringCodec(Class<T> cls) {
            this.cls = cls;
            this.registry = DEFAULT_REGISTRY;
            this.bsonTypeCodecMap = new BsonTypeCodecMap(new BsonTypeClassMap(), registry);
            this.valueTransformer = value -> value;
        }

        @Override
        public T decode(BsonReader reader, DecoderContext context) {
            try {
                T t = cls.newInstance();
                Map<String, Field> map = fieldToMap(t);
                reader.readStartDocument();

                String fieldName = "";
                try {
                    while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                        fieldName = reader.readName();
                        if (fieldName.equals("_id")) {
                            reader.skipValue();
                            continue;
                        }
                        Field field = map.get(fieldName);
                        Object value = readValue(reader, context, field);
                        field.set(t, value);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                reader.readEndDocument();
                return t;
            } catch (InstantiationException | IllegalAccessException e) {
                throw new RuntimeException(e);
            }
        }

        private Map<String, Field> fieldToMap(Object o) {
            return Arrays.stream(o.getClass().getDeclaredFields())
                    .peek(f -> f.setAccessible(true))
                    .collect(Collectors.toMap(Field::getName, f -> f));
        }

        private Object readValue(BsonReader reader, DecoderContext context, Field field) {
            BsonType bsonType = reader.getCurrentBsonType();
            if (bsonType == BsonType.NULL) {
                reader.readNull();
                return null;
            } else if (bsonType == BsonType.DECIMAL128) {
                Decimal128 decimal128 = reader.readDecimal128();
                return decimal128.bigDecimalValue();
            } else if (bsonType == BsonType.DOCUMENT) {
                return readObject(reader, context, field);
            } else if (bsonType == BsonType.ARRAY) {
                return readList(reader, context, field);
            } else if (bsonType == BsonType.BINARY
                    && BsonBinarySubType.isUuid(reader.peekBinarySubType())
                    && reader.peekBinarySize() == 16) {
                return registry.get(UUID.class).decode(reader, context);
            } else if (field.getType() == LocalDateTime.class) {
                String s = reader.readString();
                return LocalDateTime.parse(s);
            }
            Codec<?> codec = bsonTypeCodecMap.get(bsonType);
            return valueTransformer.transform(codec.decode(reader, context));
        }

        private Object readObject(BsonReader reader, DecoderContext context, Field field) {
            try {
                Class<?> actual;
                if (field.getGenericType() instanceof ParameterizedType) {
                    ParameterizedType type = (ParameterizedType) field.getGenericType();
                    actual = (Class<?>) type.getActualTypeArguments()[0];
                } else {
                    actual = field.getType();
                }
                Object o = actual.newInstance();
                Map<String, Field> map = fieldToMap(o);
                reader.readStartDocument();

                String fieldName = "";
                try {
                    while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                        fieldName = reader.readName();
                        if (fieldName.equals("_id")) {
                            reader.skipValue();
                            continue;
                        }
                        Field f = map.get(fieldName);
                        Object v = readValue(reader, context, f);
                        f.set(o, v);
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }

                reader.readEndDocument();
                return o;
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }

        private Object readList(BsonReader reader, DecoderContext context, Field field) {
            reader.readStartArray();
            List<Object> list = new ArrayList<>();
            while (reader.readBsonType() != BsonType.END_OF_DOCUMENT) {
                list.add(readValue(reader, context, field));
            }
            reader.readEndArray();
            return list;
        }

        @Override
        public void encode(BsonWriter writer, T t, EncoderContext context) {
            Map<String, Object> map = toMap(t);
            writeMap(writer, map, context);
        }

        private void writeMap(BsonWriter writer, Map<String, Object> map, EncoderContext context) {
            writer.writeStartDocument();

            beforeFields(writer, context, map);

            for (Map.Entry<String, Object> entry : map.entrySet()) {
                if (skipField(context, entry.getKey())) {
                    continue;
                }
                writer.writeName(entry.getKey());
                writeValue(writer, context, entry.getValue());
            }

            writer.writeEndDocument();
        }

        private void beforeFields(BsonWriter writer, EncoderContext context, Map<String, Object> map) {
            if (context.isEncodingCollectibleDocument() &&
                    map.containsKey(ID_FIELD_NAME)) {
                writer.writeName(ID_FIELD_NAME);
                writeValue(writer, context, map.get(ID_FIELD_NAME));
            }
        }

        private boolean skipField(final EncoderContext context, final String key) {
            return context.isEncodingCollectibleDocument() && key.equals(ID_FIELD_NAME);
        }

        @SuppressWarnings({"unchecked", "rawtypes"})
        private void writeValue(BsonWriter writer, EncoderContext context, Object value) {
            if (value == null) {
                writer.writeNull();
            } else if (value instanceof Iterable) {
                writeIterable(writer, ((Iterable<Object>) value), context.getChildContext());
            } else if (value instanceof Map) {
                writeMap(writer, (Map<String, Object>) value, context.getChildContext());
            } else if (value instanceof BaseEntity) {
                writeEntity(writer, value, context.getChildContext());
            } else {
                Codec codec = registry.get(value.getClass());
                context.encodeWithChildContext(codec, writer, value);
            }
        }

        private void writeEntity(BsonWriter writer, Object value, EncoderContext childContext) {
            Map<String, Object> map = toMap(value);
            writeMap(writer, map, childContext);
        }

        private void writeIterable(BsonWriter writer, Iterable<Object> list, EncoderContext context) {
            writer.writeStartArray();
            for (Object value : list) {
                writeValue(writer, context, value);
            }
            writer.writeEndArray();
        }

        private Map<String, Object> toMap(Object o) {
            return Arrays.stream(o.getClass()
                            .getDeclaredFields())
                    .peek(f -> f.setAccessible(true))
                    .filter(f -> {
                        try {
                            return Objects.nonNull(f.get(o));
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException(e);
                        }
                    })
                    .collect(Collectors.toMap(Field::getName, f -> {
                        try {
                            return f.get(o);
                        } catch (IllegalAccessException e) {
                            throw new RuntimeException(e);
                        }
                    }));
        }

        @Override
        public Class<T> getEncoderClass() {
            return cls;
        }
    }

}
